<?php
/**
 * Fetion.php
 *
 * Fetion PHP 版 核心处理类.
 * 适用飞信客户端版本: 飞信2008 3.3.0370 版
 * 声明: 本程序仅为个人学习研究之用, 参考部分网友提供的资料.
 * 本人未对中国移动的飞信软件做任何探测, 破解之行为.
 * 仅以模拟数据模式向飞信服务器发送信息.
 * 本人尊重中国移动的知识产权. 如有损坏中国移动利益之处请尽快通知我.
 * 我立即停止使用该程序.
 *
 * 程序需要服务器运行 PHP5 环境.
 * 必须的扩展要支持 curl, sockets, iconv 模块.
 * 短信内容编码如果不是UTF-8的编码, 支持中文的话需要在创建对象时指定短信内容编码.
 * 并且在生成对象的时候指定
 * Usage:
 * $myPhone = '13838383838';		//发送方的手机号码
 * $myPasswd = '888888';		//发送方的飞信密码
 * $toPhone = '13939393939';		//接收方的手机号码
 * $message = 'This is a test message!';		//信息内容.
 *
 * $sms = new App_Util_Fetion($myPhone, $myPasswd);	//如果信息是其他格式的编码; 像这样用: new Fetion($myPhone, $myPasswd, "GB2312");
 * if(!$sms->sendMsg($toPhone, $message)) {	//发送
 *		echo $sms->getErrorMsg();	//打印错误信息
 * } else {	//发送成功
 * 		echo 'Message Sended';
 * }
 *
 *
 * 本源码来自 Mm Framework 1.0 代码.
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://momoger.com/license
 *
 * @category	App
 * @package 	App_Util
 * @author 		Guya
 * @version    	1.0
 * @copyright  	Copyright © 2009 www.momoger.com All Rights Reserved.
 * @license    	http://momoger.com/license     New BSD License
 *
 */

/**
 * 飞信PHP版功能类
 *
 * @category	App
 * @package 	App_Util
 * @copyright  	Copyright © 2009 www.momoger.com All Rights Reserved.
 * @license    	http://momoger.com/license     New BSD License
 */
class App_Util_Fetion {

	/**
	 * 信息来源编码.
	 *
	 * @var string
	 */
	private $charset = NULL;

	/**
	 * 请求数据对象
	 *
	 * @var App_Util_Fetion_Curl
	 */
	private $request = NULL;

	/**
	 * 飞信用户的手机号码.
	 *
	 * @var string
	 */
	private $phone = NULL;

	/**
	 * 飞信用户的密码.
	 *
	 * @var string
	 */
	private $passwd = NULL;

	/**
	 * 飞信客户端版本信息
	 *
	 * @var string
	 */
	private $clientVersion = NULL;

	/**
	 * 飞信服务器地址
	 *
	 * @var string
	 */
	private $fetionUrl = NULL;

	/**
	 * 飞信服务器域名
	 *
	 * @var string
	 */
	private $fetionHost = NULL;

	/**
	 * 错误信息
	 *
	 * @var string
	 */
	private $error = NULL;

	/**
	 * 是否打印调试信息
	 *
	 * @var boolean
	 */
	private $debug = false;

	/**
	 * 构造函数.
	 *
	 * @param int $phone 飞信用户手机号码
	 * @param string $passwd 飞信用户密码
	 * @param string $charset 发送编码方式
	 */
	public function __construct($phone, $passwd, $charset = 'UTF-8') {
		$this->phone = $phone;
		$this->passwd = $passwd;
		$this->charset = $charset;

		$this->clientVersion = '3.3.0370';

		//$this->clientVersion = '3.5.1170';


		$this->log('飞信初始化信息',
				   '手机号: ' . $phone .
				   ' 登录密码: ' . $passwd .
				   ' 客户端版本: ' . $this->clientVersion .
				   ' 信息编码: ' . $charset, 'a+');

		$this->fetionUrl = 'http://nav.fetion.com.cn/nav/getsystemconfig.aspx';

		$this->fetionHost = 'fetion.com.cn';
	}

	/**
	 * 发送信息
	 *
	 * @param string $toPhone 接收手机号码
	 * @param string $msg 发送短信内容
	 * @return boolean
	 */
	public function sendMsg($toPhone, $msg = 'Hello, Baby!') {
		//validate the receiver mobile phone number
		if (!preg_match('/\d{11}/', $toPhone)) {
			$this->setErrorMsg('接收方手机号码无效', '手机号码 ' . $toPhone . ' 不是有效的移动手机号码!');
			return FALSE;
		}

		//validate the send message
		if (!$msg) {
			$this->setErrorMsg('信息无效', '不允许发送空信息!');
			return FALSE;
		}
		if (strlen($msg) > 180) {
			$this->setErrorMsg('信息无效', '短信息最长不能超过180个字符!');
			return FALSE;
		}

		//iconv message content
		if ($this->charset && 'UTF-8' != $this->charset && function_exists('iconv')) {
			$msg = @iconv($this->charset, 'UTF-8', $msg);
		}

		//send message
		return $this->sendMessage($toPhone, $msg);
	}

	/**
	 * 获取错误信息
	 *
	 * @return string
	 */
	public function getErrorMsg() {
		return $this->error;
	}

	/**
	 * 发送信息
	 *
	 * @param string $toPhone 接收手机号码
	 * @param string $msg 发送短信内容
	 * @return boolean
	 */
	private function sendMessage($toPhone, $msg) {
		//=================== start login =====================
		//validate the sender mobile phone number
		if (!preg_match('/\d{11}/', $this->phone)) {
			$this->setErrorMsg('用户手机号码无效', '手机号码 ' . $this->phone . ' 不是有效的移动手机号码!');
			return FALSE;
		}

		//validate the sender fetion password
		if (!preg_match('/\w+/', $this->passwd)) {
			$this->setErrorMsg('用户飞信密码无效', '飞信密码 ' . $this->passwd . ' 不是有效的飞信登录密码!');
			return FALSE;
		}

		//log client config data
		$this->log('发送客户端 config 数据', $this->getClientConfig());

		//set the curl request option
		$this->getRequest()->setRequestOption($this->getClientConfig(), $this->fetionUrl, $this->clientVersion);

		//curl request the fetion server
		if(!$this->getRequest()->request()) {
			$this->setErrorMsg('向服务器端请求 config 信息错误', $this->getRequest()->getErrorMsg());
			return FALSE;
		}

		//get the reponse
		$reponse = $this->getRequest()->getReponse();

		//log the reponse
		$this->log('向服务器端 config 信息请求响应数据', $reponse);

		//parse the ssi-app-sign-in url
		$matches = array();
		preg_match("/<ssi-app-sign-in([-a-zA-Z0-9]*)>([^<]*)<\/ssi-app-sign-in([-a-zA-Z0-9]*)>/i", $reponse, $matches);
		$ssiAppSignIn = @$matches[2];

		//check the sipc proxy result
		if (!$ssiAppSignIn) {
			$this->setErrorMsg('服务器 config 响应数据无效', '无法获取 ssi-app-sign-in 数据!');
			return FALSE;
		}

		//log the sipc proxy data
		$this->log('获取 ssi-app-sign-in 地址', $ssiAppSignIn);

		//parse the sipc proxy url
		$matches = array();
		preg_match("/<sipc-proxy>([^<]*)<\/sipc-proxy>/i", $reponse, $matches);
		$sipcProxy = @$matches[1];

		//check the sipc proxy result
		if (!$sipcProxy) {
			$this->setErrorMsg('服务器 config 响应数据无效', '无法获取 sipc-proxy 数据!');
			return FALSE;
		}

		//log the sipc proxy data
		$this->log('获取 sipc-proxy 地址', $sipcProxy);

		//use sign in data
		$signInData = sprintf("mobileno=%s&pwd=%s", $this->phone, $this->passwd);

		//log sign in data
		$this->log('发送获取用户飞信帐号数据', $signInData);

		//set the curl request option
		$this->getRequest()->setRequestOption($signInData, $ssiAppSignIn, $this->clientVersion);

		//curl request the fetion server
		if(!$this->getRequest()->request()) {
			$this->setErrorMsg('向服务器端发送获取用户飞信帐号数据错误', $this->getRequest()->getErrorMsg());
			return FALSE;
		}

		//get the reponse
		$reponse = $this->getRequest()->getReponse();

		//log the reponse
		$this->log('服务器端返回获取用户飞信帐号请求数据', $reponse);

		$matches = array();
		preg_match("/<user uri=\"sip:([0-9]+)@fetion.com.cn;p=[0-9]+\"/i", $reponse, $matches);
		$fetionAccount = @$matches[1];

		//check the fetion account result
		if (!$fetionAccount) {
			$this->setErrorMsg('服务器 获取用户飞信帐号 响应数据无效', '无法获取 飞信帐号 数据!');
			return FALSE;
		}

		//log the fetion account data
		$this->log('获取用户飞信帐号', $fetionAccount);

		//get the login init data
		$loginInitData = $this->getLoginData();

		//log the loign init data
		$this->log('登录初始化数据', $loginInitData);

		//set the request data for login
		$loginRequestData = sprintf("R %s SIP-C/2.0\r\nF: %s\r\nI: 1\r\nQ: 1 R\r\nL: %s\r\n\r\n", $this->fetionHost, $fetionAccount, strlen($loginInitData)) . $loginInitData;

		//log the request data for login
		$this->log('向服务器发送登录数据', $loginRequestData);

		///////////////////////////////////////////////////////////////
		$server = explode(':', $sipcProxy);
		$socketHandle = fsockopen($server[0], $server[1], $errno, $error, 90);
		if (!$socketHandle) {
			$this->setErrorMsg('向服务器端发送用户登录数据错误', 'fsockopen false: ' . $errno . "\r\n" . $error);
			return FALSE;
		}

/*
		//set the curl request option
		$this->getRequest()->setRequestOption($loginRequestData, $sipcProxy);

		//curl request the fetion server
		if(!$this->getRequest()->request()) {
			$this->setErrorMsg('向服务器端发送用户登录数据错误', 'request url: ' . $sipcProxy . "\r\n" . $this->getRequest()->getErrorMsg());
			return FALSE;
		}

		//get the reponse
		$reponse = $this->getRequest()->getReponse();
*/
		fwrite($socketHandle, $loginRequestData);
		$reponse = fread($socketHandle,20480);

		//log the reponse
		$this->log('服务器端返回用户登录请求数据', $reponse);

		//get the nonce
		preg_match("/nonce=\"([^\"]{32})\"/i", $reponse, $matches);
		$nonce = @$matches[1];

		if (!$nonce) {
			$this->setErrorMsg('服务器响应 用户登录 数据无效', '无法获取 nonce 数据!');
			return FALSE;
		}

		//log nonce data
		$this->log('服务器返回 nonce 数据', $nonce);

		//get the nonce data
		$nonceData = $this->getNonceData($fetionAccount, $nonce);

		//log the nonce data
		$this->log('cnonce nonce and data', $nonceData['cnonce'] . '\r\n' . $nonceData['nonce']);

		//reset the login data
		$loginRequestData = sprintf("R %s SIP-C/2.0\r\nF: %s\r\nI: 1\r\nQ: 2 R\r\nA: Digest response=\"%s\",cnonce=\"%s\"\r\nL: %s\r\n\r\n", $this->fetionHost, $fetionAccount, $nonceData['nonce'], $nonceData['cnonce'], strlen($loginInitData)) . $loginInitData;

		//log the request data for login
		$this->log('向服务器再次发送登录数据', $loginRequestData);

/**
		//set the curl request option
		$this->getRequest()->setRequestOption($loginRequestData, $sipcProxy);

		//curl request the fetion server
		if(!$this->getRequest()->request()) {
			$this->setErrorMsg('向服务器端再次发送用户登录数据错误', $this->getRequest()->getErrorMsg());
			return FALSE;
		}

		//get the reponse
		$reponse = $this->getRequest()->getReponse();
*/
		fwrite($socketHandle, $loginRequestData);
		$reponse = fread($socketHandle,20480);

		//log the reponse
		$this->log('服务器端再次返回用户登录请求数据', $reponse);
		//=============================== end login =====================================

		//=============================== start send message ================================
		//format fetion send message data
		$smsData = sprintf(
					"M %s SIP-C/2.0\r\nF: %s\r\nI: 2\r\nQ: 1 M\r\nT: tel:%s\r\nN: SendSMS\r\nL: %s\r\n\r\n",
					$this->fetionHost,
					$fetionAccount,
					$toPhone,
					strlen($msg)
				) . $msg;

		//log the send data
		$this->log('向服务器发送发送短信数据', $smsData);
/**
		//set the curl request option
		$this->getRequest()->setRequestOption($smsData, $sipcProxy);

		//curl request the fetion server
		if(!$this->getRequest()->request()) {
			$this->setErrorMsg('向服务器端发送发送短信数据错误', $this->getRequest()->getErrorMsg());
			return FALSE;
		}

		//get the reponse
		$reponse = $this->getRequest()->getReponse();
*/
		fwrite($socketHandle, $smsData);
		$reponse = fread($socketHandle,20480);

		//log the reponse
		$this->log('服务器端返回发送短信请求数据', $reponse);
		//================================= end send message =====================================

		//================================= start log out =======================================
		//set logout request data
		$logoutRequestData = sprintf("R %s SIP-C/2.0\r\nF: %s\r\nI: 1 \r\nQ: 3 R\r\nX: 0\r\n\r\n", $this->fetionHost, $fetionAccount);

		//log logout request data
		$this->log('向服务器发送用户退出数据', $logoutRequestData);
/**
		//set the curl request option
		$this->getRequest()->setRequestOption($logoutRequestData, $sipcProxy);

		//execute request
		$this->getRequest()->request();

		//get the reponse
		$reponse = $this->getRequest()->getReponse();
*/
		fwrite($socketHandle, $logoutRequestData);
		$reponse = fread($socketHandle,20480);
		fclose($socketHandle);

		//log the reponse
		$this->log('服务器端返回用户退出请求数据', $reponse);
		//================================= end log out ======================================

		return TRUE;
	}

	/**
	 * 组织 nonce 数据
	 *
	 * @param string $fetion_account 飞信帐号
	 * @param string $nonce nonce
	 * @return array
	 */
	private function getNonceData($fetion_account, $nonce) {
		$cnonce = strtoupper(md5(rand()));
		$key = md5("{$fetion_account}:{$this->fetionHost}:{$this->passwd}", true);
		$h1 = strtoupper(md5("{$key}:{$nonce}:{$cnonce}"));
		$h2 = strtoupper(md5("REGISTER:{$fetion_account}"));
		return array('cnonce' => $cnonce, 'nonce' => strtoupper(md5("{$h1}:{$nonce}:{$h2}")));
	}

	/**
	 * 组织登录数据
	 *
	 * @return string
	 */
	private function getLoginData() {
		$loginXml = '<args>';
		//$loginXml .= '<device type="PC" version="36" client-version="' . $this->clientVersion . '" />';
		$loginXml .= '<device type="PC" version="0" client-version="' . $this->clientVersion . '" />';
		$loginXml .= '<caps value="simple-im;im-session;temp-group;personal-group" />';
		$loginXml .= '<events value="contact;permission;system-message;personal-group" />';
		$loginXml .= '<user-info attributes="all" />';
		$loginXml .= '<presence>';
		$loginXml .= '<basic value="400" desc="" />';
		$loginXml .= '</presence>';
		$loginXml .= '</args>';
		return $loginXml;
	}

	/**
	 * 获取客户端配置信息
	 *
	 * @return string
	 */
	private function getClientConfig() {
		$clientConfig = '<config>';
		$clientConfig .= '<user mobile-no="' . $this->phone . '" />';
		$clientConfig .= '<client type="PC" version="' . $this->clientVersion . '" platform="W5.1" />';
		$clientConfig .= '<servers version="0" />';
		$clientConfig .= '<service-no version="0" />';
		$clientConfig .= '<parameters version="0" />';
		$clientConfig .= '<hints version="0" />';
		$clientConfig .= '<http-applications version="0" />';
		$clientConfig .= '<client-config version="0" />';
		$clientConfig .= '</config>';
		return $clientConfig;
	}

	/**
	 * 设置错误信息
	 *
	 * @param string $name 错误信息标题
	 * @param string $msg 错误信息内容
	 */
	private function setErrorMsg($name = '', $msg = '') {
		$this->error = '[' . $name . ']: ' . $msg;
		$this->log($name, $msg);
	}

	/**
	 * 获取数据发送对象
	 *
	 * @return App_Util_Fetion_Curl
	 */
	private function getRequest() {
		if (null === $this->request) {
			$this->request = new App_Util_Fetion_Curl();
		}
		return $this->request;
	}

	/**
	 * 调试记录飞信登录过程信息
	 *
	 * @param string $log_name log标题
	 * @param string $log_content log内容
	 */
	private function log($log_name = '', $log_content = '', $mode = 'a+') {
		if ($this->debug) {
			$fileUri = '/tmp/fetionLog.txt';
			if (!$handle = fopen($fileUri, $mode)) {
				echo "不能打开文件 $fileUri";
				exit;
			}

	   		$data = "\r\n[ " . date("Y-m-d H:i:s") . ' ' . $log_name . " ]\r\n" . str_replace("><",">\r\n<", $log_content) . "\r\n\r\n";

	   		if (fwrite($handle, $data) === FALSE) {
	    		fclose($handle);
	        	echo "不能写入到文件 $fileUri";
	        	exit;
	    	}

	    	fclose($handle);
		}
	}
}